package cn.jcby.app.lebu.service;

import cn.jcby.app.config.WxConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.*;


/**
 * @author: lihaotian
 * @since: 2024/1/3 21:03
 * @description
 **/
@Service
@Slf4j
public class PayService {
    @Autowired
    private WxConfig wxConfig;

    /**
     * 创建微信预支付订单
     *
     * @param body   商品描述
     * @param out_trade_no   订单号
     * @param price    价格 /元
     * @param ip IP地址
     * @return
     * @throws Exception
     */
    public Map<String, String> createOrderApp(String body,
                                              String out_trade_no,
                                              String price,
                                              String ip,
                                              String attach) throws Exception {
        String uuid = UUID.randomUUID().toString();
        /**
         * 生成微信「统一下单」请求数据
         */
        Map<String, String> dataMap = new HashMap<>();
        dataMap.put("appid", wxConfig.getAppId());
        dataMap.put("mch_id", wxConfig.getMchId());
        dataMap.put("nonce_str", uuid);
        dataMap.put("body", body);
        dataMap.put("attach", attach);
        dataMap.put("out_trade_no", out_trade_no);
        dataMap.put("total_fee", price);
        dataMap.put("spbill_create_ip", ip);
        dataMap.put("notify_url", wxConfig.getNotifyUrlHost());
        dataMap.put("trade_type", wxConfig.getTradeTypeApp());
        /**
         * 生成签名(MD5 编码)
         */
        String md5Sign = getMD5Sign(dataMap, wxConfig.getPaySignKey(), wxConfig.getSign());
        dataMap.put(wxConfig.getSign(), md5Sign);

        log.debug("微信支付请求参数：" + dataMap.toString());
        /**
         * 发送请求数据
         */
        String respXml = requestWithoutCert(wxConfig.getUnifiedOrder(), dataMap, 5000, 10000);
        /**
         * 解析微信返回数据
         */
        dataMap.clear();
        dataMap = processResponseXml(respXml);
        //dataMap.put("noncestr", uuid);
        log.debug("微信支付返回参数：" + dataMap.toString());
        /**
         * 没有生成预支付订单,直接返回
         */
        if (dataMap.get("prepay_id") == null) {
            throw new RuntimeException(dataMap.get("return_msg") + ";" + dataMap.get("err_code_des"));
        }
        /**
         * 生成微信APP支付「调起支付接口」的请求参数
         */
        Map<String, String> resultMap = getPayOrder(dataMap, wxConfig.getPaySignKey(),wxConfig.getSign(), wxConfig.getTradeTypeApp());
        /**
         * 添加预支付订单创建成功标识
         */
        resultMap.put("pre_pay_order_status", "success");

        /**
         * 返回处理结果
         */
        return resultMap;
    }

    /**
     * 向微信发送请求
     * @param data
     * @param useCert 是否使用证书
     * @return
     * @throws Exception
     */
    public String withdrawal(String url, String data, boolean useCert) throws Exception {
        BasicHttpClientConnectionManager connManager;
        if (useCert) {
            // 证书
            char[] password = wxConfig.getMchId().toCharArray();
            //因为我司多租户模式，会存在多个商户，所以做了商户的维护，证书通过byte[]存储
            //InputStream certStream = new ByteArrayInputStream(wxConfig.getCertfile());
            KeyStore ks = KeyStore.getInstance("PKCS12");
            //ks.load(certStream, password);

            // 实例化密钥库 & 初始化密钥工厂
            KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
            kmf.init(ks, password);

            // 创建 SSLContext
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());

            SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(
                    sslContext,
                    new String[]{"TLSv1"},
                    null,
                    new DefaultHostnameVerifier());

            connManager = new BasicHttpClientConnectionManager(
                    RegistryBuilder.<ConnectionSocketFactory>create()
                            .register("http", PlainConnectionSocketFactory.getSocketFactory())
                            .register("https", sslConnectionSocketFactory)
                            .build(),
                    null,
                    null,
                    null
            );
        }
        else {
            connManager = new BasicHttpClientConnectionManager(
                    RegistryBuilder.<ConnectionSocketFactory>create()
                            .register("http", PlainConnectionSocketFactory.getSocketFactory())
                            .register("https", SSLConnectionSocketFactory.getSocketFactory())
                            .build(),
                    null,
                    null,
                    null
            );
        }

        HttpClient httpClient = HttpClientBuilder.create()
                .setConnectionManager(connManager)
                .build();

        HttpPost httpPost = new HttpPost(url);

        RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(wxConfig.getSocketTimeout()).setConnectTimeout(wxConfig.getConnectTimeout()).build();
        httpPost.setConfig(requestConfig);

        StringEntity postEntity = new StringEntity(data, "UTF-8");
        httpPost.addHeader("Content-Type", "text/xml");
        httpPost.setEntity(postEntity);

        HttpResponse httpResponse = httpClient.execute(httpPost);
        HttpEntity httpEntity = httpResponse.getEntity();
        return EntityUtils.toString(httpEntity, "UTF-8");
    }

    /**
     * 生成微信支付「调起支付接口」请求参数
     *
     * @param data
     *            源数据
     * @param key
     *            签名密钥
     * @param fieldSign
     *            签名字段名(固定值 sign)
     * @return
     * @throws Exception
     */
    private Map<String, String> getPayOrder(Map<String, String> data, String key, String fieldSign, String tradeType)	throws Exception {
        Map<String, String> resultMap = new HashMap<>();
        resultMap.put("appid", data.get("appid"));
        resultMap.put("partnerid", data.get("mch_id"));
        resultMap.put("prepayid", data.get("prepay_id"));
        resultMap.put("package", wxConfig.getTradeTypeApp().equals(tradeType) ? "Sign=WXPay" : "prepay_id=" + data.get("prepay_id"));
        resultMap.put("noncestr", data.get("nonce_str"));
        resultMap.put("timestamp", getTimeStampSecond());
        //resultMap.put("mweburl", data.get("mweb_url"));
        //resultMap.put("codeurl", data.get("code_url"));
        //resultMap.put("signType", "MD5");
        /**
         * 生成签名
         */
        String sign = getMD5Sign(resultMap, key, fieldSign);
        resultMap.put("sign", sign);

        return resultMap;
    }

    /**
     * 获取当前时间戳(秒级,10位数字)
     *
     * @return 时间戳
     */
    private String getTimeStampSecond() {
        return Math.round((int)(System.currentTimeMillis()/1000)) + "";
    }
    /**
     * 处理 HTTPS API返回数据，转换成Map对象。return_code为SUCCESS时，验证签名。
     *
     */
    private Map<String, String> processResponseXml(String xmlStr) throws Exception {
        String RETURN_CODE = "return_code";
        String return_code;
        Map<String, String> respData = xml2Map(xmlStr);
        if (respData.containsKey(RETURN_CODE)) {
            return_code = respData.get(RETURN_CODE);
        } else {
            throw new Exception(String.format("No `return_code` in XML: %s", xmlStr));
        }

        if ("FAIL".equals(return_code)) {
            return respData;
        } else if ("SUCCESS".equals(return_code)) {
            /**
             * 签名校验
             */
            if (signValidate(respData, wxConfig.getPaySignKey(), wxConfig.getSign())) {
                return respData;
            } else {
                throw new Exception(String.format("Invalid sign value in XML: %s", xmlStr));
            }
        } else {
            throw new Exception(String.format("return_code value %s is invalid in XML: %s", return_code, xmlStr));
        }
    }

    /**
     * 校验签名
     *
     * @param data
     *            待校验数据
     * @param key
     *            密钥
     * @param fieldSign
     *            签名字段(sign)
     * @return
     */
    private boolean signValidate(Map<String, String> data, String key, String fieldSign) {
        if (!data.containsKey(fieldSign)) {
            return false;
        }
        String sign = data.get(fieldSign);
        return getMD5Sign(data, key, fieldSign).equalsIgnoreCase(sign);
    }

    /**
     * 生成 md5 签名.将 map 数据按照 key 的升序进行排列 使用URL键值对的格式(即key1=value1&key2=value2…)
     * 传送的sign参数不参与签名
     *
     * @param data
     *            待签名数据
     * @param key
     *            密钥
     * @param fieldSign
     *            签名字段(sign)
     * @return 签名
     */
    private String getMD5Sign(final Map<String, String> data, String key, String fieldSign) {
        Set<String> keySet = data.keySet();
        String[] keyArray = keySet.toArray(new String[keySet.size()]);
        Arrays.sort(keyArray);
        StringBuilder sb = new StringBuilder();
        for (String k : keyArray) {
            if (k.equals(fieldSign)) {
                continue;
            }
            if (data.get(k) != null && data.get(k).trim().length() > 0) // 参数值为空，则不参与签名
            {
                sb.append(k).append("=").append(data.get(k).trim()).append("&");
            }
        }
        sb.append("key=").append(key);
        return md5(sb.toString()).toUpperCase();
    }

    /**
     * 普通 md5 编码
     *
     * @param url
     * @return
     */
    private String md5(String url) {
        try {
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            md5.update(url.getBytes("UTF-8"));
            byte messageDigest[] = md5.digest();

            StringBuffer hexString = new StringBuffer();
            for (int i = 0; i < messageDigest.length; i++) {
                String t = Integer.toHexString(0xFF & messageDigest[i]);
                if (t.length() == 1) {
                    hexString.append("0" + t);
                } else {
                    hexString.append(t);
                }
            }
            return hexString.toString();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 不需要证书的请求
     *
     * @param strUrl
     *            String
     * @param reqData
     *            向wxpay post的请求数据
     * @param connectTimeoutMs
     *            超时时间，单位是毫秒
     * @param readTimeoutMs
     *            超时时间，单位是毫秒
     * @return API返回数据
     * @throws Exception
     */
    private String requestWithoutCert(String strUrl, Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
        String UTF8 = "UTF-8";
        String reqBody = map2Xml(reqData);
        URL httpUrl = new URL(strUrl);
        HttpURLConnection httpURLConnection = (HttpURLConnection) httpUrl.openConnection();
        httpURLConnection.setDoOutput(true);
        httpURLConnection.setRequestMethod("POST");
        httpURLConnection.setConnectTimeout(connectTimeoutMs);
        httpURLConnection.setReadTimeout(readTimeoutMs);
        httpURLConnection.connect();
        OutputStream outputStream = httpURLConnection.getOutputStream();
        outputStream.write(reqBody.getBytes(UTF8));

        // 获取内容
        InputStream inputStream = httpURLConnection.getInputStream();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, UTF8));
        final StringBuffer stringBuffer = new StringBuffer();
        String line = null;
        while ((line = bufferedReader.readLine()) != null) {
            stringBuffer.append(line).append("\n");
        }
        String resp = stringBuffer.toString();
        if (stringBuffer != null) {
            try {
                bufferedReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (outputStream != null) {
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return resp;
    }

    /**
     * 将 xml 转换为 map
     *
     * @param xmlStr
     *            xml 格式字符串
     * @return map 对象
     * @throws DocumentException
     */
    @SuppressWarnings("rawtypes")
    private static Map<String, String> xml2Map(String xmlStr) throws DocumentException {
        Map<String, String> map = new HashMap<String, String>();
        Document doc = DocumentHelper.parseText(xmlStr);

        if (doc == null) {
            return map;
        }
        Element root = doc.getRootElement();
        for (Iterator iterator = root.elementIterator(); iterator.hasNext();) {
            Element e = (Element) iterator.next();
            map.put(e.getName(), e.getText());
        }
        return map;
    }

    /**
     * map 转 xml (仅微信支付xml格式)
     *
     * @param dataMap
     *            map 数据
     * @return
     */
    private static String map2Xml(Map<String, String> dataMap) {
        StringBuffer sb = new StringBuffer();
        sb.append("<xml>");
        for (String key : dataMap.keySet()) {
            String value = "<![CDATA[" + dataMap.get(key) + "]]>";
            sb.append("<" + key + ">" + value + "</" + key + ">");
        }
        sb.append("</xml>");

        return sb.toString();
    }

}
